Skip to content

fix(chat): make theming actually work (0.0.5)#166

Merged
blove merged 1 commit into
mainfrom
claude/chat-05
May 2, 2026
Merged

fix(chat): make theming actually work (0.0.5)#166
blove merged 1 commit into
mainfrom
claude/chat-05

Conversation

@blove
Copy link
Copy Markdown
Contributor

@blove blove commented May 2, 2026

Summary

Theming was advertised but didn't work. Smoke-test investigation against the published @ngaf/chat@0.0.4 surfaced two related bugs:

  1. Consumer's :root { --ngaf-chat-primary: ...; } override was ignored. The 0.0.4 architecture set tokens on every chat component's :host. Direct-on-element token settings shadow CSS inheritance from ancestor :root regardless of specificity. `[_nghost-c123] { --ngaf-chat-primary: dark; }` (set on the chat element) always won over `:root { --ngaf-chat-primary: blue; }` (inherited).

  2. [data-ngaf-chat-theme=\"dark\"] attribute didn't switch dark mode. `:host([data-ngaf-chat-theme="dark"])` only matched the component's own host. Setting the attribute on ``/``/any wrapper had no effect. Even setting it on `` directly, child chat components (chat-input, chat-message, chat-window, ...) re-set light tokens on their own :host and shadowed it anyway.

Fix

Stop setting tokens on `:host`. Inject the defaults onto `:root` once via a shared `<style id="ngaf-chat-root-tokens">` element, appended to `` on first chat-tokens module evaluation (idempotent, SSR-guarded).

Wrap the default rules in `@layer ngaf-chat` so consumer's unlayered `:root { --ngaf-chat-*: ... }` wins regardless of source order — the standard CSS pattern for framework defaults.

```ts
// libs/chat/src/lib/styles/chat-tokens.ts
const ROOT_TOKEN_STYLES = `
@layer ngaf-chat {
:root { --ngaf-chat-primary: rgb(28, 28, 28); /* ...defaults... / }
@media (prefers-color-scheme: dark) {
:root { --ngaf-chat-primary: rgb(255, 255, 255); /
... / }
}
:root[data-ngaf-chat-theme="dark"],
[data-ngaf-chat-theme="dark"] { /
dark tokens / }
/
...etc... */
}
`;

export function ensureChatRootStyles(): void {
if (typeof document === 'undefined') return;
if (document.getElementById('ngaf-chat-root-tokens')) return;
const style = document.createElement('style');
style.id = 'ngaf-chat-root-tokens';
style.textContent = ROOT_TOKEN_STYLES;
document.head.appendChild(style);
}

ensureChatRootStyles(); // auto-invoke on module evaluation
```

Validation

End-to-end against the smoke app:

Scenario Before After
Default load light theme renders light theme renders ✓
`:root { --ngaf-chat-primary: rgb(80, 0, 200); }` chatPrimary = rgb(28, 28, 28) (default, override ignored) chatPrimary = rgb(80, 0, 200) ✓
`document.body.setAttribute('data-ngaf-chat-theme', 'dark')` still light full dark theme propagates ✓
`document.documentElement.setAttribute('data-ngaf-chat-theme', 'dark')` still light full dark theme propagates ✓
`prefers-color-scheme: dark` (untested previously) dark theme by default

Test plan

  • All chat unit tests pass
  • `nx build chat` clean
  • `nx lint chat` clean
  • Theme override + dark mode validated manually against the smoke app
  • CI green
  • Tag v0.0.5 to publish

🤖 Generated with Claude Code

The 0.0.4 architecture set design tokens on every chat component's
:host. Two consequences that broke the documented theming contract:

1. Direct-on-host token settings shadow CSS inheritance from :root,
   regardless of specificity. So a consumer's
   `:root { --ngaf-chat-primary: blue; }` (specificity 0,0,1) was
   ignored — `[_nghost-c123] { --ngaf-chat-primary: dark; }` (0,1,0,
   targeting the chat element directly) won.

2. `:host([data-ngaf-chat-theme="dark"])` only matched the component's
   own host, not ancestors. And every child component (chat-input,
   chat-message, chat-window, ...) re-set light tokens on its own
   :host, shadowing parent attribute switches anyway.

Fix: stop setting tokens on :host. Inject the defaults onto `:root`
once via a shared `<style id="ngaf-chat-root-tokens">` element,
appended to <head> on first chat-component module evaluation
(idempotent, SSR-guarded). Wrap the rule set in `@layer ngaf-chat`
so consumer's unlayered `:root { ... }` wins regardless of source
order — the standard CSS pattern for framework defaults.

Theme switching now works on any ancestor: setting
`[data-ngaf-chat-theme="dark"]` on `<html>`, `<body>`, or any wrapper
flips dark via inherited custom properties (no more :host override
plumbing needed).

Validated end-to-end against the published smoke app:
- Default light tokens load on :root ✓
- `:root { --ngaf-chat-primary: rgb(80, 0, 200); }` consumer override
  applies to chat element (chatPrimary becomes rgb(80, 0, 200)) ✓
- `document.body.setAttribute('data-ngaf-chat-theme', 'dark')` flips
  the entire chat to dark ✓

Bumps @ngaf/chat to 0.0.5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cacheplane Ready Ready Preview, Comment May 2, 2026 1:32am

Request Review

@blove blove merged commit 331e125 into main May 2, 2026
14 checks passed
@blove blove deleted the claude/chat-05 branch May 7, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant